﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Epi;
using Epi.Data;
using CDC.SampleShell.Infrastructure;
using CDC.SampleShell.Domain.Events;

/// Erik Knudsen 2015

namespace CDC.SampleShell.Domain
{
    /// <summary>
    /// Example of how to inherit from an Epi.Project and extend its functionality
    /// </summary>
    public sealed class Project : Epi.Project, IDisposable
    {
        /* Projects are structured as such:
         * 
         *  PROJECT Object (represented as a .PRJ file in the file system and by the Epi.Project class in the API)
         *      - POINTER - The project contains a pointer (connection string) to the database and a string representation of the data driver, which is used to instantiate the appropriate data driver for the database type at runtime.
         *      - VIEWS - Projects contain one or more data entry forms (forms are called "Views" in the Epi Info 7 API)
         *          - PAGES - Each view may contain one or more pages. The VIEW contains a collection of pages.
         *          - FIELDS - Each view contains a collection of fields. The VIEW contains a collection of fields. Each field has a pointer back to its corresponding page and view.
         *      - DATA - Views may or may not have data associated with them. If they do, then those data are represented by data tables in the database, where each form has 1 'base' table and then one additional table for each page on the form.
         *      - PGM - Classic Analysis scripts, also called "PGMs" (short for 'Programs', which comes from the MS-DOS versions of Epi Info from the 80's and 90's) may be stored directly in the project file.
         * 
         * In the Epi Info 7 API, the Epi.Project class contains a collection of Views. Each 
         * View contains a collection of both fields in that view and pages in the view. However,
         * there is no data model in the Epi.Project class. Epi Info 7 was designed to load
         * one record at a time and never needed core-level data loading functionality. To 
         * cache all the data from the database at run-time, we would have to extend the project
         * class (as has been done in this example) to add a collection of record objects. The
         * way this is done is by using the project class's POINTER to the database to execute a
         * SELECT query. The results of that query give us a System.Data.DataTable which we can
         * iterate over to populate a collection of Records.
         * 
         * The POINTER referenced here is an encrypted connection string in the .PRJ file and
         * an IDbDriver in the Project class. The constructor for Epi.Project automatically
         * creates the correct data driver for the database the project is associated with.
         * All we need to do to run a query is build the query and execute it against the
         * data driver; we're using a repository class to handle that, so the logic for that
         * can be found in the Infrastructure layer.
         */

        #region Members
        private FormRecordDictionary _records = new FormRecordDictionary();
        private readonly ChangesetChecker _changesetChecker;
        #endregion // Members

        #region Properties
        public System.Guid Id { get; private set; }
        private IRepository<IEntity> Repository { get; set; }
        public XDocument Document { get; private set; }
        private IDbDriver Database { get { return this.CollectedData.GetDatabase(); } }
        #endregion // Properties

        #region Events
        public event RecordInsertedFromServerEventHandler RecordInsertedFromServer;
        public event RecordUpdatedFromServerEventHandler RecordUpdatedFromServer;
        #endregion Events

        #region Constructors
        /// <summary>
        /// The call to base() creates the data driver associated with the project (either the MS Access driver or
        /// MS SQL Server driver, depending on the contents of the PRJ file) and creates all of the necessary
        /// in-memory metadata structures.
        /// </summary>
        /// <param name="filePath">The path to the PRJ file</param>
        public Project(string filePath)
            : base(filePath)
        {
            // preconditions
            Contract.Requires(!String.IsNullOrEmpty(filePath));

            // postconditions
            Contract.Ensures(Id != null);
            Contract.Ensures(Document != null);
            Contract.Ensures(Repository != null);

            #region Run the database upgrader module

            IDatabaseUpgrader dbUpgrader = null;

            // Epi Info 7 only supports these two DB types for data entry; future iterations of this class
            // may wish to look into IoC to handle creation of DB-specific modules. Also, note that this
            // process is automated and in the current design, runs without user interaction. There may
            // be scenarios where the user should be prompted that a database upgrade is about to take place.
            // The pattern here may have to change if that is to be supported.
            if (Database.ToString().Contains("Sql", StringComparison.OrdinalIgnoreCase))
            {
                dbUpgrader = new SqlDatabaseUpgrader(Database);
            }
            else
            {
                dbUpgrader = new MdbDatabaseUpgrader(Database);
            }

            dbUpgrader.UpgradeDatabase();

            #endregion // Upgrade database

            #region Create changeset modules
            if (Database.ToString().Contains("Sql", StringComparison.OrdinalIgnoreCase))
            {
                _changesetChecker = new ChangesetChecker(Database, 250);
                _changesetChecker.LockRecordReceived += ChangesetChecker_LockRecordReceived;
                _changesetChecker.LockRecordReceived += ChangesetChecker_UnlockRecordReceived;
                _changesetChecker.UpdateRecordReceived += ChangesetChecker_UpdateRecordReceived;
                _changesetChecker.InsertRecordReceived += ChangesetChecker_InsertRecordReceived;
            }
            #endregion // Create changeset modules

            Document = XDocument.Load(filePath);
            DbLogger.Database = Database;

            Id = new System.Guid(Document.Root.Attribute("id").Value);

            // The repository class is our wrapper for the Epi Info 7 data driver. See the MdbRecordRepository class
            // for details on how to use the IDbDriver to carry out typical CRUD operations.
            Repository = new RecordRepository(this);

            this.LoadViews();
        }
        #endregion // Constructors

        #region Event Handlers
        private void ChangesetChecker_LockRecordReceived(object sender, Infrastructure.Events.ChangesetReceivedEventArgs e)
        {
            Record record = _records.GetRecordByGuid(e.GlobalRecordId);
            record.Lock();
        }

        private void ChangesetChecker_UnlockRecordReceived(object sender, Infrastructure.Events.ChangesetReceivedEventArgs e)
        {
            Record record = _records.GetRecordByGuid(e.GlobalRecordId);
            record.Unlock();
        }

        private void ChangesetChecker_UpdateRecordReceived(object sender, Infrastructure.Events.ChangesetReceivedEventArgs e)
        {
            RefreshRecord(e.GlobalRecordId);

            if (RecordUpdatedFromServer != null)
            {
                View form = GetViewById(e.FormId);
                RecordUpdatedFromServer(this, new RecordUpdatedFromServerArgs(form, _records.GetRecordByGuid(e.GlobalRecordId)));
            }
        }

        private void ChangesetChecker_InsertRecordReceived(object sender, Infrastructure.Events.ChangesetReceivedEventArgs e)
        {
            View form = GetViewById(e.FormId);
            Record newRecord = Repository.FindOne(form, e.GlobalRecordId) as Record;

            if (newRecord != null)
            {
                _records[form].Add(newRecord);

                if (RecordInsertedFromServer != null)
                {
                    RecordInsertedFromServer(this, new RecordInsertedFromServerArgs(form, newRecord));
                }
            }
        }
        #endregion // Event Handlers

        #region Methods

        public override void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (metadata != null)
                {
                    metadata = null;
                }
                if (collectedData != null)
                {
                    collectedData = null;
                }
                if (views != null)
                {
                    views = null;
                }

                if (_changesetChecker != null)
                {
                    _changesetChecker.StopPolling();
                    _changesetChecker.LockRecordReceived -= ChangesetChecker_LockRecordReceived;
                    _changesetChecker.LockRecordReceived -= ChangesetChecker_UnlockRecordReceived;
                    _changesetChecker.UpdateRecordReceived -= ChangesetChecker_UpdateRecordReceived;
                    _changesetChecker.InsertRecordReceived -= ChangesetChecker_InsertRecordReceived;
                    _changesetChecker.Dispose();
                }
            }
            // free native resources if there are any.
        }

        /// <summary>
        /// Gets an enumerable set of record objects for a given form in the project
        /// </summary>
        /// <param name="form">The form whose records should be returned</param>
        /// <returns>IEnumerable; represents all records associated with this form</returns>
        public IEnumerable<Record> GetRecords(View form)
        {
            Contract.Requires(form != null);
            Contract.Requires(this.Views.Contains(form));

            return _records[form].AsEnumerable();
        }

        /// <summary>
        /// Repopulates all records
        /// </summary>
        public void RepopulateRecordCollection()
        {
            if (_changesetChecker != null && _changesetChecker.IsEnabled)
            {
                _changesetChecker.StopPolling();
            }

            _records.Clear();

            // Important: Epi Info 7's data model does not have a mechanism by which to load all records. Epi Info 7
            // was instead designed to load one record at a time in data entry mode. I work around this limitation
            // by extending the Epi.Project class with the CDC.SampleShell.Domain.Project class, which includes in the
            // constructor this call to create a collection of records for all forms. For real-world implementations
            // of sample Epi Info 7 shells, you would likely be changing the following few lines to point to a different
            // form, or set of forms, and perhaps use different collection classes to store the records.
            
            foreach (View form in Views)
            {
                _records.Add(form, new List<Record>());
                foreach (Record record in Repository.FetchAll(form))
                {
                    _records[form].Add(record);
                }
            }

            if (_changesetChecker != null && !_changesetChecker.IsEnabled)
            {
                _changesetChecker.StartPolling();
            }
            
        }

        /// <summary>
        /// Refreshes an in-memory record object with the corresponding row in the database
        /// </summary>
        /// <param name="globalRecordId">The ID of the record to update</param>
        public void RefreshRecord(Guid globalRecordId)
        {
            Contract.Requires(globalRecordId != null);

            Record recordToUpdate = _records.GetRecordByGuid(globalRecordId);

            if (recordToUpdate != null)
            {
                recordToUpdate.OverwriteWith(Repository.FindOne(recordToUpdate.Form, globalRecordId));
            }
        }

        private void DeleteRecord(Record record)
        {
            Contract.Requires(record != null);

            Repository.UnitOfWork.Delete(record);
            Repository.UnitOfWork.Commit();
        }

        /// <summary>
        /// Hard-deletes a record from the database and removes it from the project's record collection
        /// </summary>
        /// <param name="globalRecordId"></param>
        public void DeleteRecord(Guid globalRecordId)
        {
            Contract.Requires(globalRecordId != null);

            Record recordToDelete = _records.GetRecordByGuid(globalRecordId);

            if (recordToDelete != null)
            {
                _records[recordToDelete.Form].Remove(recordToDelete);
                DeleteRecord(recordToDelete);
            }
        }

        public void Legacy_RecordAdded(string guid)
        {
            Contract.Requires(!String.IsNullOrEmpty(guid));

            // For records added via Epi Info 7
            ChangesetSender sender = new ChangesetSender(Database);
            sender.SendMessage(ChangesetType.InsertRecord, "Record inserted", guid);
        }

        public void Legacy_RecordUpdated(string guid)
        {
            Contract.Requires(!String.IsNullOrEmpty(guid));

            // For records updated via Epi Info 7
            ChangesetSender sender = new ChangesetSender(Database);
            sender.SendMessage(ChangesetType.UpdateRecord, "Record updated", guid);
        }

        public void Legacy_RecordLocked(string guid)
        {
            Contract.Requires(!String.IsNullOrEmpty(guid));

            // For records added via Epi Info 7
            ChangesetSender sender = new ChangesetSender(Database);
            sender.SendMessage(ChangesetType.LockRecord, "Record locked", guid);
        }

        public void Legacy_RecordUnlocked(string guid)
        {
            Contract.Requires(!String.IsNullOrEmpty(guid));

            // For records updated via Epi Info 7
            ChangesetSender sender = new ChangesetSender(Database);
            sender.SendMessage(ChangesetType.UnlockRecord, "Record updated", guid);
        }

        #endregion // Methods
    }
}
